package cs275.ants.agent;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
//import java.util.Stack;
import java.util.logging.Logger;

import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;

import cs275.ants.graph.antgraph.AntGraphEdge;
import cs275.ants.graph.antgraph.AntGraphVertex;
import cs275.ants.graph.base.Graph;
import cs275.ants.graph.base.GraphEdge;
import cs275.ants.graph.base.GraphVertex;
import cs275.ants.graphics.Drawable;
import cs275.ants.shared.CycleFreeStack;

public class Ant extends Drawable implements Serializable {

	/**
	 * Penalty for moving backwards
	 */
	private static final double BACK_PENALTY = .0001;
	private static final long serialVersionUID = 5788053423283856733L;
	private static final Logger logger = Logger.getLogger(Ant.class.getName());
	private GraphVertex currentPosition;
	private Graph graph;
	private Random random;
	private AntMode mode = AntMode.SEARCH;
	private GraphVertex originalPosition;
	static final double ALPHA = 1.0;
	private boolean dropPheromone = false;
	
	private CycleFreeStack<GraphVertex> vertexStack;
	
	public enum AntMode {
		SEARCH, RETURN
	}

	public Ant(Graph graph, GraphVertex currentPosition) {
		this(graph, currentPosition, new Random(System.currentTimeMillis()));
	}
	
	// Visible for testing
	Ant(Graph graph, GraphVertex currentPosition, Random random) {
		this.currentPosition = currentPosition;
		this.originalPosition = currentPosition;
		this.graph = graph;
		this.random = random;
		
		this.vertexStack = new CycleFreeStack<GraphVertex>();
	}
	
	public boolean move(GraphVertex destination) {
		if(canMove(destination)) {
			if (mode != AntMode.RETURN) {
				vertexStack.push(currentPosition);
			}
			currentPosition = destination;
			logger.finest(String.format("*****Visited Stack: %s", vertexStack.toString()));
			
			checkModeConditions();
			return true;
		}
		return false;
	}
	
	private boolean isAtOriginalPosition() {
		return currentPosition.equals(originalPosition);
	}
	
	public AntMode getMode() {
		return mode;
	}
	
	public GraphVertex getCurrentPosition() {
		return currentPosition;
	}

	public boolean isAtFood() {
		// Need to be able to check if food here.
		if (this.currentPosition instanceof AntGraphVertex) { 
			return ((AntGraphVertex) this.currentPosition).hasFood();
		} else {
			return false;
		}
	}
	
	private boolean canMove(GraphVertex destination) {
		HashSet<GraphEdge> edges = graph.getEdgesByVertex(currentPosition);
		if(edges == null) {
			return false;
		}

		for(GraphEdge edge : edges) {
			// First check that the edge exists
			// If edge exists, check that the destination is one of the two nodes.
			if(edge != null && (edge.getNode1() == destination || edge.getNode2() == destination)) {
				return true;
			}
		}
		return false;
	}
	
	public GraphVertex chooseNextMove() throws IllegalStateException {
		if(mode == AntMode.SEARCH) {
			HashSet<GraphEdge> edges = graph.getEdgesByVertex(currentPosition);
			
			excretePheromoneIfSeesPheromone(edges);
			
			if(edges.size() == 0) {
				throw new IllegalStateException("The current position has no neighbors");
			}
			
			double totalCost = 0.0;
			Set<RangedEdge> ranges = Sets.newLinkedHashSet();
			for(GraphEdge edge : edges) {
				double cost = edge.getCost();
				
				
				// Penalyze backwards movements
				if (!vertexStack.empty()) {
					GraphVertex previousLocation = vertexStack.peek();
					if (edge.getNode1().equals(previousLocation)
							|| edge.getNode2().equals(previousLocation)) {
						cost = cost * BACK_PENALTY;
					}
				}
				
				cost = Math.pow(cost, ALPHA);
				ranges.add(new RangedEdge(edge, totalCost, totalCost + cost));
				totalCost += cost;
			}
//			logger.finest(ranges.toString());
			
			double comparisonValue = random.nextDouble() * totalCost;
			for(RangedEdge range : ranges) {
				if(range.isInRange(comparisonValue)) {
					GraphEdge edge = range.getEdge();
					if(!edge.getNode1().equals(currentPosition)) {
						logger.fine(String.format("Ant chose %s", edge.getNode1()));
						if(dropPheromone) {
							depositPheromone(edge.getNode1(), .1);
						}
						return edge.getNode1();
					} else {
						logger.fine(String.format("Ant chose %s", edge.getNode2()));
						if(dropPheromone) {
							depositPheromone(edge.getNode2(), .01);
						}
						return edge.getNode2();
					}
				}
			}
			throw new IllegalStateException("Didn't find a best edge to follow");
		}
		if (mode == AntMode.RETURN) {
			GraphVertex nextVertex = vertexStack.pop();
			if(dropPheromone) {
				depositPheromone(nextVertex, 1);
			} else {
				throw new IllegalStateException("We're returning and for some reason not dropping pheromone.");
			}
			// FOR EACH edge in the intersection, deposit pheromone
			return nextVertex;
		}
		
		throw new IllegalStateException("Unknown antMode: " + mode);
	}

	private void checkModeConditions() {
		if(isAtOriginalPosition()) {
			mode = AntMode.SEARCH;
			vertexStack = new CycleFreeStack<GraphVertex>();
		}
		
		if(isAtFood()) {
			mode = AntMode.RETURN;
			dropPheromone = true;
		}
	}

	private void excretePheromoneIfSeesPheromone(HashSet<GraphEdge> edges) {
		for(GraphEdge edge : edges) {
			if (edge.getCost() > 1.0) {
				dropPheromone = true;
			}
		}
	}

	private void depositPheromone(GraphVertex nextVertex, double pheromone) {
		HashSet<GraphEdge> edgesFromCurrent = graph.getEdgesByVertex(currentPosition);
		HashSet<GraphEdge> edgesFromNextVertex = graph.getEdgesByVertex(nextVertex);
		SetView<GraphEdge> edges = Sets.intersection(edgesFromCurrent, edgesFromNextVertex);
		for(GraphEdge edge : edges) {
			AntGraphEdge antEdge = (AntGraphEdge) edge;
			antEdge.depositPheromone(pheromone);
			
			logger.fine(String.format("*****Deposited pheromone at %s", antEdge));
		}
	}

	@Override
	public void draw() {
		// TODO Add draw logic here
	}

	class RangedEdge {
		private double start;
		private double end;
		private GraphEdge edge;

		public RangedEdge(GraphEdge edge, double start, double end) {
			this.start = start;
			this.end = end;
			this.edge = edge;
		}
		public double getEnd() {
			return end;
		}
		public double getStart() {
			return start;
		}
		public GraphEdge getEdge() {
			return edge;
		}
		
		public boolean isInRange(double value) {
			if (value >= start && value < end) {
				return true;
			}
			return false;
		}
		@Override
		public String toString() {
			return Objects.toStringHelper(this)
				.add("start", start)
				.add("end", end)
				.add("edge", edge)
				.toString();
		}
	}
}
